# question.py

import os
import sys
import gettext

Root_dir = os.path.dirname(
             os.path.dirname(
               os.path.dirname(os.path.abspath(__file__))))
#print "Root_dir", Root_dir

Locale_dir = os.path.join(Root_dir, 'translation', '0.1')
#print "Locale_dir", Locale_dir

from naimath.engine import extracted_questions

Map = {}  # {domain: lookup_fn}   -- set by caller (web.py)

Questions = {}      # {(category, q_name): question}

def load_translations(*languages):
    domains = set(q_info[1] for q_info in extracted_questions.Questions)
    domains.add('diagnosis')
    domains.add('standard')
    for domain in domains:
        tr = gettext.translation(domain, Locale_dir, languages,
                                 codeset = 'UTF-8')
        Map[domain] = tr.gettext

    # create all question objects:
    for q_info in extracted_questions.Questions:
        q_name, category, q_type = q_info[:3]
        if (category, q_name) in Questions:
            raise AssertionError("duplicate question: %s.%s" % (category,
                                 q_name))
        try:
            q_class = Classes[q_type]
        except KeyError:
            raise ValueError("illegal question type, %s, for %s.%s question" %
                               (q_type, category, q_name))
        Questions[category, q_name] = \
          q_class(category, q_name, tuple(c[0] for c in q_info[3:]))

def diagnosis(name):
    return Map['diagnosis'](name)

def lookup(category, q_name):
    return Questions[category, q_name]

def read_canned_questions(filename):
    with open(filename) as f:
        questions = {}
        top_list = read_categories(questions, filename,
                                              iter(ignore_comments(f)))[0]
        return questions, top_list

def read_categories(questions, filename, lines, indent = -1):
    r'''Returns the top list of questions, updates 'questions'.

    The returned questions are (category, question_name), which are also the
    keys in questions.

    It also returns the lineno, indent_level, stripped_line of the next line
    after the categories at this indent level.  Returns (None, None, None) at
    EOF.

    'questions' is {(category, question_name):
                      {answer: [(category, question_name)...]}}

        >>> questions = {}
        >>> read_categories(questions, 'some_file',
        ...                 iter(ignore_comments(('top',))), 0)
        ([], 1, 0, 'top')
        >>> questions
        {}
    '''
    my_indent = None
    top_list = []
    lineno, new_indent, line = next(lines, (None, None, None))
    while lineno is not None:
        if my_indent is None:
            if new_indent <= indent: break
            my_indent = new_indent
        elif new_indent < my_indent:
            break
        elif new_indent != my_indent:
            raise SyntaxError("Improper indent level for category",
                              (filename, lineno, new_indent, line))
        q_list, lineno, new_indent, line = \
          read_questions(line, questions, filename, lines, my_indent)
        top_list.extend(q_list)
    return top_list, lineno, new_indent, line

def read_questions(category, questions, filename, lines, indent):
    r'''Reads questions under a category.

    Returns question_list, lineno, indent, stripped_line.  Question_list is a
    list of (category, question_name).  The lineno, indent and stripped line
    are for the next line after the questions.  This will be all None values
    at EOF.

        >>> questions = {}
        >>> read_questions('cat', questions, 'some_file',
        ...                iter(ignore_comments((
        ...                  '  q1',
        ...                  '  q2',
        ...                  'top',
        ...                ))), 0)
        ([('cat', 'q1'), ('cat', 'q2')], 3, 0, 'top')
        >>> questions
        {}
    '''
    my_indent = None
    top_list = []
    lineno, new_indent, line = next(lines, (None, None, None))
    while lineno is not None:
        if my_indent is None:
            if new_indent <= indent:
                raise SyntaxError("Missing Questions",
                                  (filename, lineno, new_indent, line))
            my_indent = new_indent
        elif new_indent < my_indent:
            break
        elif new_indent != my_indent:
            raise SyntaxError("Improper indent level for question",
                              (filename, lineno, new_indent, line))
        q = category, line
        top_list.append(q)
        lineno, new_indent, line = \
          read_answers(q, questions, filename, lines, my_indent)
    return top_list, lineno, new_indent, line

def read_answers(q, questions, filename, lines, indent):
    r'''Reads answers and subquestions for question 'q'.

    Returns the lineno, indent, stripped_line of the next line after the
    answers.  This will be all None values at EOF.

        >>> questions = {}
        >>> read_answers(('cat', 'q1'), questions, 'some_file',
        ...                iter(ignore_comments((
        ...                  '    a1',
        ...                  '      cat2',
        ...                  '        q3',
        ...                  '    a2',
        ...                  '      cat4',
        ...                  '        q5',
        ...                  '        q6',
        ...                ))), 2)
        (None, None, None)
        >>> for q, answers in sorted(questions.iteritems()):
        ...     print q
        ...     for a, q_list in sorted(answers.iteritems()):
        ...          print " ", a, q_list
        ('cat', 'q1')
          a1 [('cat2', 'q3')]
          a2 [('cat4', 'q5'), ('cat4', 'q6')]
    '''
    my_indent = None
    lineno, new_indent, line = next(lines, (None, None, None))
    while lineno is not None:
        if my_indent is None:
            if new_indent <= indent: break
            answers = {}
            questions[q] = answers
            my_indent = new_indent
        elif new_indent < my_indent:
            break
        elif new_indent != my_indent:
            raise SyntaxError("Improper indent level for answer",
                              (filename, lineno, new_indent, line))
        answer = line
        nested_questions, lineno, new_indent, line = \
          read_categories(questions, filename, lines, my_indent)
        answers[answer] = nested_questions
    return lineno, new_indent, line

def ignore_comments(f):
    r'''Generate lines from f, ignoring blank lines and comments.

    Yields lineno, indent_level, stripped_line:

        >>> tuple(ignore_comments((' \n', '  # comment', '    hi\n')))
        ((3, 4, 'hi'),)
    '''
    for lineno, line in enumerate(f, 1):
        indent, stripped = indent_level(line)
        if stripped and stripped[0] != '#':
            yield lineno, indent, stripped

def indent_level(line):
    r'''Returns indent_level and stripped line.

        >>> indent_level('    hello    \n')
        (4, 'hello')
    '''
    line = line.rstrip()
    stripped = line.lstrip()
    return len(line) - len(stripped), stripped

class question(object):
    r'''Base class for all questions.

    All of the methods here, except __init__, are obsolete.
    '''
    translate_choices = True

    def __init__(self, category, q_name, choices):
        self.category = category
        self.q_name = q_name
        self.question = self.lookup_question()

        self.choices = tuple((choice, self.lookup_choice(choice))
                             for choice in choices)

    def lookup_question(self):
        try:
            return Map[self.category](self.q_name)
        except KeyError:
            raise ValueError("illegal category: " + self.category)

    def lookup_choice(self, choice):
        try:
            return Map[self.category]("%s:%s" % (self.q_name, choice))
        except KeyError:
            raise ValueError("illegal category: " + self.category)

    def ask(self):
        while True:
            ans_text = self.get_answer()
            try:
                ans = self.translate_answer(ans_text)
                print 'answer for %s.%s is %r' % \
                        (self.category, self.q_name, ans)
                return ans
            except ValueError, e:
                print >> sys.stderr, e
            self.print_retry()

    def print_retry(self):
        print "Illegal answer: try again"

    def radio_input(self, value, tr):
        return '<input type="radio" name="%s" value="%s" /> %s' % \
                 (self.q_name, value, tr)

    def get_input_elements(self):
        r'''Returns [input element].
        '''
        return [self.radio_input(v, tr) for v, tr in self.choices.iteritems()]

class single_choice(question):
    r'''Single answer from a list of choices.
    '''
    prompt = 'Select one answer: '

    def get_answer(self):
        print self.question
        for i, (_, answer) in enumerate(self.choices, start=1):
            print i, answer
        return raw_input(self.prompt)

    def translate_answer(self, ans):
        i = int(ans.strip())
        if i < 1 or i > len(self.choices):
            raise ValueError("choice out of range, select 1-%d" %
                             len(self.choices))
        return self.choices[i-1][0]

class multiple_choices(single_choice):
    r'''Multiple answers from a list of choices.
    '''
    prompt = 'Select as many as apply, separated by commas: '

    def translate_answer(self, ans):
        if ans:
            return tuple(super(multiple_choices, self).translate_answer(a)
                         for a in ans.split(','))
        else:
            return ()

class yes_no(single_choice):
    r'''Simple yes/no question.
    '''
    choices = ('yes', 'no')
    translate_choices = False

    def __init__(self, category, q_name, choices):
        super(yes_no, self).__init__(category, q_name, self.choices)

    def lookup_choice(self, choice):
        try:
            return Map['standard'](choice)
        except KeyError:
            raise ValueError("missing translation: standard")

class positive_negative(yes_no):
    r'''Looking for positive/negative answer (e.g., for results of lab tests).
    '''
    choices = ('positive', 'negative')

class textinput(question):
    r'''Answer is simply text that the user enters.
    '''
    def get_answer(self):
        return raw_input(self.question + ' ')

    def translate_answer(self, ans):
        return ans

Classes = {
    'single_choice': single_choice,
    'multiple_choices': multiple_choices,
    'textinput': textinput,
    'yes_no': yes_no,
    'positive_negative': positive_negative,
}

#class question(object):
#    def __init__(self, name, question, f):
#        self.name = name
#        self.question = question
#        self.init2(f)
#
#    def init2(self, f):
#        pass
#
#    @classmethod
#    def from_file(cls, filename, ans = None):
#        if ans is None: ans = {}
#        with read_lines(open(filename)) as f:
#            for line, lineno in f:
#                print "from_file got:", line, lineno
#                assert not line[0].isspace(), \
#                       "%s(%d): indenting not allowed" % (filename, lineno)
#                name, question = line.split(None, 1)
#                ans[name] = cls(name, question, f)
#        return ans
#
#    def ask(self):
#        while True:
#            ans = raw_input(self.question)
#            try:
#                return self.convert(ans)
#            except ValueError:
#                pass
#            self.print_retry()
#
#class yn(question):
#    def convert(self, ans):
#        ans = ans.lower()
#        if ans in ('t', 'true', 'y', 'yes'): return True
#        if ans in ('f', 'false', 'n', 'no'): return False
#        raise ValueError
#
#    def print_retry(self):
#        print "Illegal answer: enter 'yes' or 'no' (or 'y' or 'n')"
#
#class fill_in_the_blank(question):
#    def convert(self, ans):
#        try:
#            return int(ans)
#        except ValueError:
#            try:
#                return float(ans)
#            except ValueError:
#                return ans
#
#class choice(question):
#    def init2(self, f):
#        self.choices = {}
#        for line, lineno in f:
#            if not line[0].isspace():
#                f.push(line)
#                print "choice", self.name, "terminated with", line
#                return
#            name, description = line.split(None, 1)
#            self.choices[name] = description
#
#    def ask(self):
#        print self.question
#        choices = sorted(self.choices.iteritems(),
#                         key=lambda (label, desc): desc)
#        for i, (label, description) in enumerate(choices):
#            print "  %d: %s" % (i + 1, description)
#        while True:
#            ans = raw_input("? ")
#            try:
#                return choices[int(ans) - 1][0]
#            except ValueError:
#                pass
#            self.print_retry()
#
#    def print_retry(self):
#        print "Illegal entry: enter a number between 1 and", len(self.choices)

class read_lines(object):
    def __init__(self, file):
        self.file = file
        self.fileiter = iter(file)
        self.line = None
        self.lineno = 1

    def close(self):
        self.file.close()

    def __enter__(self):
        return self

    def __exit__(self, exc_type=None, exc_val=None, exc_tb=None):
        self.close()

    def __iter__(self):
        return self

    def next(self):
        if self.line is not None:
            line = self.line
            self.line = None
            return line, self.lineno
        for line in self.fileiter:
            line = line.rstrip()
            pound = line.find('#')
            if pound >= 0: line = line[:pound]
            if line.strip():
                return line, self.lineno
            self.lineno += 1
        raise StopIteration

    def push(self, line):
        self.line = line

